import NIOCore

extension PostgresBackendMessage {
    
    enum Field: UInt8, Hashable {
        /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message),
        /// or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a
        //// localized translation of one of these. Always present.
        case localizedSeverity = 0x53 /// S
        
        /// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message),
        /// or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message).
        /// This is identical to the S field except that the contents are never localized.
        /// This is present only in messages generated by PostgreSQL versions 9.6 and later.
        case severity = 0x56 /// V
        
        /// Code: the SQLSTATE code for the error (see Appendix A). Not localizable. Always present.
        case sqlState = 0x43 /// C
        
        /// Message: the primary human-readable error message. This should be accurate but terse (typically one line).
        /// Always present.
        case message = 0x4D /// M
        
        /// Detail: an optional secondary error message carrying more detail about the problem.
        /// Might run to multiple lines.
        case detail = 0x44 /// D
        
        /// Hint: an optional suggestion what to do about the problem.
        /// This is intended to differ from Detail in that it offers advice (potentially inappropriate)
        /// rather than hard facts. Might run to multiple lines.
        case hint = 0x48 /// H
        
        /// Position: the field value is a decimal ASCII integer, indicating an error cursor
        /// position as an index into the original query string. The first character has index 1,
        /// and positions are measured in characters not bytes.
        case position = 0x50 /// P
        
        /// Internal position: this is defined the same as the P field, but it is used when the
        /// cursor position refers to an internally generated command rather than the one submitted by the client.
        /// The q field will always appear when this field appears.
        case internalPosition = 0x70 /// p
        
        /// Internal query: the text of a failed internally-generated command.
        /// This could be, for example, a SQL query issued by a PL/pgSQL function.
        case internalQuery = 0x71 /// q
        
        /// Where: an indication of the context in which the error occurred.
        /// Presently this includes a call stack traceback of active procedural language functions and
        /// internally-generated queries. The trace is one entry per line, most recent first.
        case locationContext = 0x57 /// W
        
        /// Schema name: if the error was associated with a specific database object, the name of
        /// the schema containing that object, if any.
        case schemaName = 0x73 /// s
        
        /// Table name: if the error was associated with a specific table, the name of the table.
        /// (Refer to the schema name field for the name of the table's schema.)
        case tableName = 0x74 /// t
        
        /// Column name: if the error was associated with a specific table column, the name of the column.
        /// (Refer to the schema and table name fields to identify the table.)
        case columnName = 0x63 /// c
        
        /// Data type name: if the error was associated with a specific data type, the name of the data type.
        /// (Refer to the schema name field for the name of the data type's schema.)
        case dataTypeName = 0x64 /// d
        
        /// Constraint name: if the error was associated with a specific constraint, the name of the constraint.
        /// Refer to fields listed above for the associated table or domain. (For this purpose, indexes are
        /// treated as constraints, even if they weren't created with constraint syntax.)
        case constraintName = 0x6E /// n
        
        /// File: the file name of the source-code location where the error was reported.
        case file = 0x46 /// F
        
        /// Line: the line number of the source-code location where the error was reported.
        case line = 0x4C /// L
        
        /// Routine: the name of the source-code routine reporting the error.
        case routine = 0x52 /// R
    }
        
    struct ErrorResponse: PSQLMessageNotice, PayloadDecodable, Hashable {
        let fields: [PostgresBackendMessage.Field: String]
        
        init(fields: [PostgresBackendMessage.Field: String]) {
            self.fields = fields
        }
    }
    
    struct NoticeResponse: PSQLMessageNotice, PayloadDecodable, Hashable {
        let fields: [PostgresBackendMessage.Field: String]
        
        init(fields: [PostgresBackendMessage.Field: String]) {
            self.fields = fields
        }
    }
}

protocol PSQLMessageNotice {
    var fields: [PostgresBackendMessage.Field: String] { get }
    
    init(fields: [PostgresBackendMessage.Field: String])
}

extension PostgresBackendMessage.PayloadDecodable where Self: PSQLMessageNotice {
    
    static func decode(from buffer: inout ByteBuffer) throws -> Self {        
        var fields: [PostgresBackendMessage.Field: String] = [:]
        while let id = buffer.readInteger(as: UInt8.self) {
            if id == 0 {
                break
            }
            guard let field = PostgresBackendMessage.Field(rawValue: id) else {
                throw PSQLPartialDecodingError.valueNotRawRepresentable(
                    value: id,
                    asType: PostgresBackendMessage.Field.self)
            }
            
            guard let string = buffer.readNullTerminatedString() else {
                throw PSQLPartialDecodingError.fieldNotDecodable(type: String.self)
            }
            fields[field] = string
        }
        return Self.init(fields: fields)
    }
}

extension PostgresBackendMessage.Field: CustomStringConvertible {
    
    var description: String {
        switch self {
        case .localizedSeverity:
            return "Localized Severity"
        case .severity:
            return "Severity"
        case .sqlState:
            return "Code"
        case .message:
            return "Message"
        case .detail:
            return "Detail"
        case .hint:
            return "Hint"
        case .position:
            return "Position"
        case .internalPosition:
            return "Internal position"
        case .internalQuery:
            return "Internal query"
        case .locationContext:
            return "Where"
        case .schemaName:
            return "Schema name"
        case .tableName:
            return "Table name"
        case .columnName:
            return "Column name"
        case .dataTypeName:
            return "Data type name"
        case .constraintName:
            return "Constraint name"
        case .file:
            return "File"
        case .line:
            return "Line"
        case .routine:
            return "Routine"
        }
    }
}
